/* vim: set ts=2 et sw=2 cindent fo=qroca: */

package com.globant.google.mendoza;

import java.util.Date;
import javax.xml.bind.JAXBElement;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.globant.google.mendoza.malbec.SchemaValidator;
import com.globant.google.mendoza.malbec.SeleniumBuyerRobot;
import com.globant.google.mendoza.malbec.CartUtils;
import com.globant.google.mendoza.malbec.SeleniumBuyerRobotFactory;

import com.globant.google.mendoza.malbec.SchemaValidator.SchemaValidationResult;
import com.globant.google.mendoza.malbec.schema._2.NewOrderNotification;
import com.globant.google.mendoza.malbec.schema._2.MerchantCalculationCallback;
import com.globant.google.mendoza.malbec.schema._2.MerchantCalculationResults;
import com.globant.google.mendoza.malbec.schema._2.RiskInformationNotification;
import com.globant.google.mendoza.malbec.schema._2.ObjectFactory;

import com.globant.google.mendoza.command.MendozaAddGiftCommand;
import com.globant.google.mendoza.command.MendozaAssertCartCommand;
import com.globant.google.mendoza.command.MendozaAssertSignatureCommand;
import com.globant.google.mendoza.command.MendozaChangeMerchantCommand;
import com.globant.google.mendoza.command.MendozaCommandResult;
import com.globant.google.mendoza.command.MendozaAddCouponCommand;
import com.globant.google.mendoza.command.MendozaHelpCommand;
import com.globant.google.mendoza.command.MendozaLoginCommand;
import com.globant.google.mendoza.command.MendozaPlaceOrderCommand;
import com.globant.google.mendoza.command.MendozaCheckoutCommand;
import com.globant.google.mendoza.command.MendozaCheckoutEncodedCommand;
import com.globant.google.mendoza.command.MendozaResetCommand;
import com.globant.google.mendoza.command.MendozaStatusCommand;

/** Helps running test cases in e-commerce applications.
 *
 * This server acts as a sort of proxy between an e-commerce application and
 * google checkout. It waits for commands from the application under test such
 * as place order, post cart, etc, and then sends the equivalent command to
 * checkout.
 */
public final class MendozaServer {

  /** The class logger.
   */
  private static Log log = LogFactory.getLog(MendozaServer.class);

  /** The merchant calculation validator.
   */
  private MendozaMerchantCalculationValidator mcValidator;

  /** The buyer robot used to place the order.
   */
  private SeleniumBuyerRobot buyerRobot;

  /** The mendoza status information.
   */
  private MendozaStatus mendozaStatus;

  /** The buyer robot factory. */
  private SeleniumBuyerRobotFactory buyerRobotFactory;

  /** The received messages from Checkout. */
  private TransportProxyMessages proxyMessages =
    new TransportProxyMessages();

  /** The mendoza server state. */
  private MendozaServerState serverState;

  /** Creates an instance of mendoza.
   *
   * @param factory The selenium buyer robot factory. Cannot be null.
   *
   * @param theMerchantId The merchant id. Cannot be null.
   *
   * @param theMerchantKey The merchant key. Cannot be null.
   *
   * @param calculationsTimeout The merchant calculations timeout.
   */
  public MendozaServer(final SeleniumBuyerRobotFactory factory,
      final String theMerchantId, final String theMerchantKey,
      final int calculationsTimeout) {
    if (factory == null) {
      throw new IllegalArgumentException("The buyer factory cannot be null");
    }
    if (theMerchantId == null) {
      throw new IllegalArgumentException("The merchant id cannot be null");
    }
    if (theMerchantKey == null) {
      throw new IllegalArgumentException("The merchant key cannot be null");
    }
    buyerRobotFactory = factory;
    serverState = new MendozaServerState();
    serverState.setMerchantId(theMerchantId);
    serverState.setMerchantKey(theMerchantKey);
    mendozaStatus = new MendozaStatus();
    mcValidator = new MendozaMerchantCalculationValidator(
        calculationsTimeout);
    initBuyerRobot();
  }

  /** Sets the cart to be posted to checkout.
   *
   * @param request The Mendoza server request.
   *
   * @return Returns the command result.
   */
  public MendozaCommandResult checkoutEncoded(final MendozaRequest request) {
    log.trace("Entering checkout encoded");
    MendozaCheckoutEncodedCommand command =
      new MendozaCheckoutEncodedCommand(request, this, serverState);
    command.execute();
    log.trace("Leaving checkout encoded");
    return command.getResult();
  }

  /** Sets the cart to be posted to checkout.
   *
   * @param request The Mendoza server request.
   *
   * @return Returns the command result.
   */
  public MendozaCommandResult checkout(final MendozaRequest request) {
    log.trace("Entering checkout");
    MendozaCheckoutCommand command =
      new MendozaCheckoutCommand(request, this, serverState);
    command.execute();
    log.trace("Leaving checkout");
    return command.getResult();
  }

  /** Sets the new merchant id and key.
   *
   * @param request The Mendoza server request.
   *
   * @return Returns the command result.
   */
  public MendozaCommandResult changeMerchant(final MendozaRequest request) {
    log.trace("Entering changeMerchant");
    MendozaChangeMerchantCommand command =
      new MendozaChangeMerchantCommand(request, this, serverState);
    command.execute();
    initBuyerRobot();
    if (buyerRobot == null) {
      throw new RuntimeException("Cannot init buyer robot.");
    }
    log.trace("Leaving changeMerchant");
    return command.getResult();
  }

  /** Initializes and logs to the application as a buyer.
   *
   * @param request The Mendoza server request.
   *
   * @return Returns the command result.
   */
  public MendozaCommandResult login(final MendozaRequest request) {
    log.trace("Entering login");
    MendozaLoginCommand command =
      new MendozaLoginCommand(request, this, serverState);
    command.execute();
    log.trace("Leaving login");
    return command.getResult();
  }

  /** Adds a coupon to the order.
   *
   * @param request The Mendoza server request.
   *
   * @return Returns the command result.
   */
  public MendozaCommandResult addCoupon(final MendozaRequest request) {
    log.trace("Entering addCoupon");
    MendozaAddCouponCommand command =
      new MendozaAddCouponCommand(request, this, serverState);
    command.execute();
    log.trace("Leaving addCoupon");
    return command.getResult();
  }

  /** Adds a gift certificate to the order.
   *
   * @param request The Mendoza server request.
   *
   * @return Returns the command result.
   */
  public MendozaCommandResult addGiftCertificate(
      final MendozaRequest request) {
    log.trace("Entering addGiftCertificate");
    MendozaAddGiftCommand command =
      new MendozaAddGiftCommand(request, this, serverState);
    command.execute();
    log.trace("Leaving addGiftCertificate");
    return command.getResult();
  }

  /** Sets the Mendoza Server to the initial state.
   *
   * @param request The Mendoza server request.
   *
   * @return Returns the command result.
   */
  public MendozaCommandResult reset(final MendozaRequest request) {
    log.trace("Entering reset");
    proxyMessages = new TransportProxyMessages();
    MendozaResetCommand command =
      new MendozaResetCommand(request, this, serverState);
    command.execute();
    log.trace("Leaving reset");
    return command.getResult();
  }

  /** Gets the Mendoza Server help.
   *
   * @param request The Mendoza server request.
   *
   * @return Returns the command result.
   */
  public MendozaCommandResult help(final MendozaRequest request) {
    log.trace("Entering help");
    proxyMessages = new TransportProxyMessages();
    MendozaHelpCommand command =
      new MendozaHelpCommand(request, this, serverState);
    command.execute();
    log.trace("Leaving help");
    return command.getResult();
  }

  /** Gets the mendoza status.
   *
   * @param request The Mendoza server request.
   *
   * @return Returns the command result.
   */
  public MendozaCommandResult status(final MendozaRequest request) {
    log.trace("Entering status");
    MendozaStatusCommand command =
      new MendozaStatusCommand(request, this, serverState);
    command.execute();
    log.trace("Leaving status");
    return command.getResult();
  }

  /** Places an order to checkout.
   *
   * @param request The mendoza server request.
   *
   * @return Returns the command result.
   */
  public MendozaCommandResult placeOrder(final MendozaRequest request) {
    log.trace("Entering placeOrder");
    clearReceivedMessagesFromCheckout();
    MendozaPlaceOrderCommand command =
      new MendozaPlaceOrderCommand(request, this, serverState, buyerRobot);
    command.execute();
    log.trace("Leaving placeOrder");
    return command.getResult();
  }

  /** Asserts the validation of the cart signature.
   *
   * @param request The mendoza server request.
   *
   * @return Returns the command result.
   */
  public MendozaCommandResult assertSignature(final MendozaRequest request) {
    log.trace("Entering assertSignature");
    MendozaAssertSignatureCommand command =
      new MendozaAssertSignatureCommand(request, this, serverState);
    command.execute();
    log.trace("Leaving assertSignature");
    return command.getResult();
  }

  /** Asserts the validation of the cart.
   *
   * @param request The mendoza server request.
   *
   * @return Returns the command result.
   */
  public MendozaCommandResult assertCart(final MendozaRequest request) {
    log.trace("Entering assertCart");
    MendozaAssertCartCommand command =
      new MendozaAssertCartCommand(request, this, serverState);
    command.execute();
    log.trace("Leaving assertCart");
    return command.getResult();
  }

  /** Gets the mendoza status.
   *
   * @return Returns the mendoza server status.
   */
  public MendozaStatus getMendozaStatus() {
    if (serverState.getGoogleOrderNumber() != null) {
      updateNotifications();
    }
    return mendozaStatus;
  }

  /** Sets the merchant calculation callback and result.
   *
   * @param callbackMsg The merchant calculation callback message
   *
   * @param resultMsg The merchant calculation result.
   *
   * @param startTime The time when the calculation started.
   *
   * @param endTime The time when the calculation was responded.
   */
  public void setMerchantCalculation(final String callbackMsg,
      final String resultMsg, final Date startTime, final Date endTime) {
    log.trace("Entering setMerchantCalculation");

    SchemaValidator validator = new SchemaValidator();
    SchemaValidationResult result = validator.validate(resultMsg);
    boolean resultXMLValid = result.isXmlValid();
    String validationMessage = "Merchant calculation results XML is valid.";
    if (!resultXMLValid) {
      validationMessage = "Merchant calculation results XML is not valid.\n";
      validationMessage = validationMessage + result.getMessage();
      log.debug("Merchant calculation result message: " + resultMsg);
      log.debug("The merchant calculation result message is not a valid XML: "
      + validationMessage);
    }

    MerchantCalculationCallback merchantCalculationCallback = null;
    MerchantCalculationResults merchantCalculationResult = null;
    try {
      /* Callback */
      JAXBElement callbackNode =
        (JAXBElement) CartUtils.unmarshal(callbackMsg);
      merchantCalculationCallback =
        (MerchantCalculationCallback) callbackNode.getValue();

      if (resultXMLValid) {
        /* Result */
        JAXBElement resultNode =
          (JAXBElement) CartUtils.unmarshal(resultMsg);
        merchantCalculationResult =
          (MerchantCalculationResults) resultNode.getValue();
      }
    } catch (Exception e) {
      log.trace("Leaving setMerchantCalculation");
      throw new RuntimeException(e);
    }

    MendozaMerchantCalculation merchantCalculation =
      new MendozaMerchantCalculation(
        callbackMsg, merchantCalculationCallback, resultMsg,
        merchantCalculationResult, startTime, endTime, mcValidator);
    merchantCalculation.setResultSchemaValidation(validationMessage);

    mendozaStatus.addMerchantCalculation(merchantCalculation);
    log.trace("Leaving setMerchantCalculation");
  }

  /** Sets the new order notification message received.
    *
    * @param theNewOrderNotification The new order notification received.
    */
  private void setNewOrderNotification(
      final NewOrderNotification theNewOrderNotification) {
    log.trace("Entering setNewOrderNotification");
    String marshaled = "";
    try {
      ObjectFactory objectFactory = new ObjectFactory();
      marshaled = CartUtils.marshal(
          objectFactory.createNewOrderNotification(theNewOrderNotification),
          true);
      marshaled = marshaled.trim();
    } catch (Exception e) {
      log.trace("Leaving setMerchantCalculation");
      throw new RuntimeException(e);
    }
    mendozaStatus.setNewOrderNotification(marshaled, theNewOrderNotification);
    mendozaStatus.setInitialOrderStates(
        theNewOrderNotification.getFulfillmentOrderState().value(),
        theNewOrderNotification.getFinancialOrderState().value());
    log.trace("Leaving setNewOrderNotification");
   }

  /** Sets the risk information notification message received.
    *
    * @param riskInformation The risk information received.
    */
  private void setRiskInformationNotification(
      final RiskInformationNotification riskInformation) {
    log.trace("Entering setRiskInformationNotification");
    String marshaled = "";
    try {
      ObjectFactory objectFactory = new ObjectFactory();
      marshaled = CartUtils.marshal(
          objectFactory.createRiskInformationNotification(riskInformation),
          true);
      marshaled = marshaled.trim();
    } catch (Exception e) {
      log.trace("Leaving setRiskInformationNotification");
      throw new RuntimeException(e);
    }
    mendozaStatus.setRiskInformationNotification(marshaled, riskInformation);
    log.trace("Leaving setRiskInformationNotification");
   }

  /** Initializes the buyer robot.
   */
  private void initBuyerRobot() {
    log.trace("Entering initBuyerRobot");
    try {
      buyerRobot = buyerRobotFactory.createSeleniumBuyerRobot(
        serverState.getMerchantId(), serverState.getMerchantKey());
    } catch (Exception e) {
      log.debug(e.getMessage());
      log.trace("Leaving initBuyerRobot");
    }
    log.trace("Leaving initBuyerRobot");
  }

  /** Adds the new order notification message received.
   *
   * @param orderNumber The google order number.
   *
   * @param newOrderNotification The new order notification received.
   */
  public void addNewOrderNotification(final String orderNumber,
      final NewOrderNotification newOrderNotification) {
    log.trace("Entering addNewOrderNotification");
    proxyMessages.addNewOrderNotification(orderNumber, newOrderNotification);
    log.trace("Leaving addNewOrderNotification");
  }

  /** Adds the risk information message received.
   *
   * @param orderNumber The google order number.
   *
   * @param riskNotification The risk information notification received.
   */
  public void addRiskInformationNotification(final String orderNumber,
      final RiskInformationNotification riskNotification) {
    log.trace("Entering addRiskInformationNotification");
    proxyMessages.addRiskInformationNotification(orderNumber, riskNotification);
    log.trace("Leaving addRiskInformationNotification");
  }

  /** Sets the order state change.
   *
   * @param orderNumber The google order number.
   *
   * @param newFulfillmentState Order's current fulfillment state.
   *
   * @param newFinancialState Order's current financial state.
   */
  public void addOrderStateChanged(final String orderNumber,
      final String newFulfillmentState,
      final String newFinancialState) {
   log.trace("Entering addOrderStateChanged");
   proxyMessages.addOrderStateChanged(
       orderNumber, newFulfillmentState, newFinancialState);
   log.trace("Leaving addOrderStateChanged");
 }

  /** Sets the notifications that belongs to a specific order number.
   */
  private void updateNotifications() {
    log.trace("Entering setNotifications");
    NewOrderNotification non =
      proxyMessages.getNewOrderNotification(serverState.getGoogleOrderNumber());
    if (non != null) {
      setNewOrderNotification(non);
    }
    RiskInformationNotification risk =
      proxyMessages.getRiskInformationNotification(
          serverState.getGoogleOrderNumber());
    if (risk != null) {
      setRiskInformationNotification(risk);
    }
    String fin = proxyMessages.getFinancialState(
        serverState.getGoogleOrderNumber());
    if (fin != null) {
      mendozaStatus.setFinancialStateChanged(fin);
    }
    String ful = proxyMessages.getFulfillmentState(
        serverState.getGoogleOrderNumber());
    if (ful != null) {
      mendozaStatus.setFulfillmentStateChanged(ful);
    }
    log.trace("Leaving setNotifications");
  }

  /** Clears the messages received from Checkout.
   */
  private void clearReceivedMessagesFromCheckout() {
    log.trace("Entering clearReceivedMessagesFromCheckout");
    mendozaStatus.clearReceivedCheckoutMessages();
    proxyMessages = new TransportProxyMessages();
    serverState.setGoogleOrderNumber(null);
    log.trace("Leaving clearReceivedMessagesFromCheckout");
  }
}
